﻿
using System;
using System.Collections.Generic;

namespace EmperialApps.WeatherSpark.Data {

    /// <summary>Manages the generation and positioning of a set of visuals along a scale.</summary>
    public abstract partial class ItemManager<TItem>
        where TItem : class {

        /// <summary>Displays all in-range items along the specified scale.</summary>
        protected IList<double> DisplayItems( Scale scale, double increment, double offset, double displayLimit ) {
            if( double.IsInfinity( scale.Minimum )
             || double.IsInfinity( scale.Maximum ) )
                return new double[0];

            // Calculate all the values for the range.
            double start = GetRangeStart( scale.Minimum, increment );
            var displayValues = new List<double>( Scale.GetRangeValues( start, scale.Maximum, increment ) );
            if( scale.IsInverted )
                displayValues.Reverse( );

            // Mark all items as extras.
            Pair.Swap( ref this._items, ref this._extraItems );

            // Re-use any items with values that are still visible.
            double position;
            var reusedItems = new Dictionary<double, TItem>( );
            for( int i = this._extraItems.Count - 1; i >= 0; --i ) {
                TItem item = this._extraItems[i];
                double value = this.GetItemValue( item );
                if( reusedItems.ContainsKey( value ) || displayValues.BinarySearch( value ) < 0 )
                    continue;

                ItemVisibility visibility = GetPosition( item, value, scale, offset, displayLimit, out position );
                if( visibility == ItemVisibility.InVisibleRange ) {
                    reusedItems.Add( value, item );
                    this.UseItem( item, position );
                }
            }

            // Display remaining visible values.
            foreach( double value in displayValues ) {
                if( reusedItems.ContainsKey( value ) )
                    continue;

                TItem item = this.GetItem( value );

                ItemVisibility visibility = GetPosition( item, value, scale, offset, displayLimit, out position );
                if( visibility == ItemVisibility.AfterVisibleRange )
                    break;
                if( visibility == ItemVisibility.BeforeVisibleRange )
                    continue;

                this.UseItem( item, position );
            }

            // Hide any unused items, and save them all to the items list.
            this.HideExtraItems( );
            this._items.AddRange( this._extraItems );
            this._extraItems.Clear( );

            return displayValues;
        }

        /// <summary>Gets the first value of the range to display.</summary>
        protected virtual double GetRangeStart( double minimum, double increment ) {
            double start = Math.Ceiling( minimum / increment ) * increment;
            return start;
        }

        /// <summary>Creates a new item.</summary>
        protected abstract TItem CreateItem( );

        /// <summary>Sets the position of an item.</summary>
        protected abstract void SetItemPosition( TItem item, double itemPosition );

        /// <summary>Sets the visibility of an item.</summary>
        protected abstract void SetItemVisibility( TItem item, bool visible );

        /// <summary>Sets the data value displayed by an item.</summary>
        protected abstract void SetItemValue( TItem item, double value );

        /// <summary>Gets the data value displayed by an item.</summary>
        protected abstract double GetItemValue( TItem item );

        /// <summary>Gets the size of an item.</summary>
        protected abstract double GetItemSize( TItem item );

        /// <summary>Gets the offset from the center used to position the item.</summary>
        protected virtual double GetItemCenteringOffset( TItem item ) {
            double size = GetItemSize( item );
            return size / 2;
        }

        #region Private Members

        private List<TItem> _items = new List<TItem>( );
        private List<TItem> _extraItems = new List<TItem>( );

        private TItem GetItem( double value ) {
            if( this._extraItems.Count == 0 )
                this._extraItems.Add( this.CreateItem( ) );

            TItem item = this._extraItems[this._extraItems.Count - 1];
            this.SetItemValue( item, value );
            return item;
        }

        private void UseItem( TItem item, double position ) {
            this._items.Add( item );
            this._extraItems.Remove( item );

            this.SetItemVisibility( item, visible: true );
            this.SetItemPosition( item, position );
        }

        private void HideExtraItems( ) {
            foreach( TItem item in this._extraItems )
                this.SetItemVisibility( item, visible: false );
        }

        private ItemVisibility GetPosition( TItem item, double value, Scale scale, double offset, double displayLimit, out double position ) {
            double itemSize = this.GetItemSize( item );
            double itemCenterPosition = scale.ToScreen( value ) - offset;
            double itemCenteringOffset = this.GetItemCenteringOffset( item );
            position = itemCenterPosition - itemCenteringOffset;

            if( scale.IsInverted )
                itemCenteringOffset *= -1;

            if( itemCenterPosition + itemCenteringOffset + itemSize + this.OverhangAllowance < 0 )
                return ItemVisibility.BeforeVisibleRange;

            if( itemCenterPosition - itemCenteringOffset - this.OverhangAllowance > displayLimit )
                return ItemVisibility.AfterVisibleRange;

            return ItemVisibility.InVisibleRange;
        }


        private enum ItemVisibility {
            InVisibleRange,
            BeforeVisibleRange,
            AfterVisibleRange,
        }

        #endregion
    }

}
